﻿using Sidvall.IO.Data;
using Sidvall.Net;
using Sidvall.Net.Mime;
using Sidvall.Security;
using Sidvall.Serialization;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Sidvall.IO
{
    public class NetFileSystem : Sidvall.IO.IStorage
    {
        #region Public Members

        public bool UsePost { get; set; }

        #region AuthenticationStrategy

        private IAuthenticationStrategy _AuthenticationStrategy;
        public IAuthenticationStrategy AuthenticationStrategy
        {
            get
            {
                if (_AuthenticationStrategy != null)
                    return _AuthenticationStrategy;
                return NetSystemContext.Current.AuthenticationStrategy;
            }
            set
            {
                _AuthenticationStrategy = value;
            }
        }

        #endregion
        #region NetFileSystemStrategy

        private INetFileSystemStrategy _NetFileSystemStrategy;
        public INetFileSystemStrategy NetFileSystemStrategy
        {
            get
            {
                if (_NetFileSystemStrategy != null)
                    return _NetFileSystemStrategy;
                return NetSystemContext.Current.NetFileSystemStrategy;
            }
            set
            {
                _NetFileSystemStrategy = value;
            }
        }

        #endregion
        #region BaseAddress

        private string _BaseAddress;
        public string BaseAddress
        {
            get
            {
                if (!string.IsNullOrWhiteSpace(_BaseAddress))
                    return _BaseAddress;
                return NetSystemContext.Current.BaseAddress;
            }
            set
            {
                _BaseAddress = value;
            }
        }

        #endregion
        #region DefaultBufferSize

        private int? _DefaultBufferSize;
        public int DefaultBufferSize
        {
            get
            {
                if (_DefaultBufferSize != null)
                    return _DefaultBufferSize.Value;
                return NetSystemContext.Current.DefaultBufferSize;
            }
            set
            {
                if (value < 1)
                    _DefaultBufferSize = null;
                else
                    _DefaultBufferSize = value;
            }
        }

        #endregion
        #region StoreId

        private string _StoreId;
        public string StoreId
        {
            get
            {
                if (!string.IsNullOrWhiteSpace(_StoreId))
                    return _StoreId;
                return NetSystemContext.Current.StoreId;
            }
            set
            {
                _StoreId = value;
            }
        }

        #endregion
        #region FolderDelimiter

        public string FolderDelimiter
        {
            get
            {
                return "/";
            }
        }

        #endregion

        #region Combine

        public string Combine(params string[] paths)
        {
            if (paths == null)
                return null;
            var sb = new System.Text.StringBuilder();
            foreach (var path in paths)
            {
                if (sb.Length > 0)
                    sb.Append(this.FolderDelimiter);
                sb.Append(path.Trim(this.FolderDelimiter, System.StringComparison.Ordinal));
            }
            return sb.ToString().Replace(new string[] { "\\" }, this.FolderDelimiter, System.StringComparison.Ordinal);
        }

        #endregion
        #region GetFolderPath

        public string GetFolderPath(FolderType folder)
        {
            switch (folder)
            {
                case Sidvall.IO.FolderType.InstalledLocation:
                    return "";
                case Sidvall.IO.FolderType.LocalApplicationData:
                case Sidvall.IO.FolderType.RoamingApplicationData:
                    return "";
                case Sidvall.IO.FolderType.TemporaryApplicationData:
                    return "Temp";
                case Sidvall.IO.FolderType.Desktop:
                case Sidvall.IO.FolderType.MyDocuments:
                case Sidvall.IO.FolderType.MyMusic:
                case Sidvall.IO.FolderType.MyPictures:
                case Sidvall.IO.FolderType.MyVideos:
                    return "";
                default:
                    throw new System.ArgumentException(nameof(folder));
            }
        }

        #endregion
        #region GetPath

        /// <summary>
        /// Returns the full path for this storage:
        /// [BaseAddress]/[path]
        /// </summary>
        public string GetPath(string path)
        {
            return GetPath(path, Security.ResourceType.File, Sidvall.IO.FileSystemOperation.Read);
        }
        public string GetPath(string path, Sidvall.IO.Security.ResourceType resource, Sidvall.IO.FileSystemOperation action)
        {
            if (path != null)
                path = path.Replace(new string[] { "\\" }, this.FolderDelimiter, System.StringComparison.Ordinal);
            if (string.IsNullOrWhiteSpace(this.BaseAddress))
                return NetSystemContext.Current.GetWebAddress(this.NetFileSystemStrategy, resource, action, HttpMethod.Get, this.StoreId, path);
            if (string.IsNullOrWhiteSpace(path))
                return this.BaseAddress.Trim('/') + "/" + NetSystemContext.Current.GetWebAddress(this.NetFileSystemStrategy, resource, action, HttpMethod.Get, this.StoreId);
            if (path.StartsWith(this.BaseAddress, System.StringComparison.OrdinalIgnoreCase))
                return path;
            return this.BaseAddress.Trim('/') + "/" + NetSystemContext.Current.GetWebAddress(this.NetFileSystemStrategy, resource, action, HttpMethod.Get, this.StoreId, path);
        }

        #endregion
        #region GetStoragePath

        /// <summary>
        /// Returns the path that uniquely identifies the item to this storage:
        /// [path]
        /// </summary>
        public string GetStoragePath(string path)
        {
            if (path != null)
                path = path.Replace(new string[] { "\\" }, this.FolderDelimiter, System.StringComparison.Ordinal);
            path = Sidvall.IO.StorageGroupManager.GetPathWithoutRoot(path, this.BaseAddress, this.FolderDelimiter);
            return path;
        }

        #endregion

        #region CreateFolderAsync

        public async Task<bool> CreateFolderAsync(string path, CancellationToken cancellationToken)
        {
            path = GetStoragePath(path);
            var context = new ItemContext()
            {
                Path = path,
            };
            return await DoActionAsync<bool>(Sidvall.IO.Security.ResourceType.Folder, Sidvall.IO.FileSystemOperation.Create, context, cancellationToken).ConfigureAwait(false);
        }

        #endregion

        #region DeleteFileAsync

        public async Task<bool> DeleteFileAsync(string path, CancellationToken cancellationToken)
        {
            path = GetStoragePath(path);
            var context = new ItemContext()
            {
                Path = path,
            };
            return await DoActionAsync<bool>(Sidvall.IO.Security.ResourceType.File, Sidvall.IO.FileSystemOperation.Delete, context, cancellationToken).ConfigureAwait(false);
        }

        #endregion
        #region DeleteFolderAsync

        public async Task<bool> DeleteFolderAsync(string path, bool recursive, CancellationToken cancellationToken)
        {
            path = GetStoragePath(path);
            var context = new DeleteItemContext()
            {
                Path = path,
                Recursive = recursive,
            };
            return await DoActionAsync<bool>(Sidvall.IO.Security.ResourceType.Folder, Sidvall.IO.FileSystemOperation.Delete, context, cancellationToken).ConfigureAwait(false);
        }

        #endregion

        #region FileExistsAsync

        public async Task<bool> FileExistsAsync(string path, CancellationToken cancellationToken)
        {
            if (this.NetFileSystemStrategy != null)
            {
                var strategyResult = await this.NetFileSystemStrategy.FileExistsAsync(path, cancellationToken).ConfigureAwait(false);
                if (strategyResult != null)
                    return strategyResult.Value;
            }
            path = GetStoragePath(path);
            var context = new ItemContext()
            {
                Path = path,
            };
            return await DoActionAsync<bool>(Sidvall.IO.Security.ResourceType.File, Sidvall.IO.FileSystemOperation.Exists, context, cancellationToken).ConfigureAwait(false);
        }

        #endregion
        #region FolderExistsAsync

        public async Task<bool> FolderExistsAsync(string path, CancellationToken cancellationToken)
        {
            path = GetStoragePath(path);
            var context = new ItemContext()
            {
                Path = path,
            };
            return await DoActionAsync<bool>(Sidvall.IO.Security.ResourceType.Folder, Sidvall.IO.FileSystemOperation.Exists, context, cancellationToken).ConfigureAwait(false);
        }

        #endregion

        #region GetFileNamesAsync

        public async Task<IEnumerable<string>> GetFileNamesAsync(string path, string searchPattern, SearchOption searchOption,
            CancellationToken cancellationToken)
        {
            path = GetStoragePath(path);
            var context = new GetNamesContext()
            {
                Path = path,
                SearchOption = searchOption,
                SearchPattern = searchPattern,
            };
            var items = await DoActionAsync<IEnumerable<string>>(Sidvall.IO.Security.ResourceType.File, Sidvall.IO.FileSystemOperation.GetNames, context, cancellationToken).ConfigureAwait(false);
            return GetPaths(Sidvall.IO.Security.ResourceType.File, Sidvall.IO.FileSystemOperation.Read, items);
        }

        #endregion
        #region GetFolderNamesAsync

        public async Task<IEnumerable<string>> GetFolderNamesAsync(string path, string searchPattern, SearchOption searchOption,
             CancellationToken cancellationToken)
        {
            path = GetStoragePath(path);
            var context = new GetNamesContext()
            {
                Path = path,
                SearchOption = searchOption,
                SearchPattern = searchPattern,
            };
            var items = await DoActionAsync<IEnumerable<string>>(Sidvall.IO.Security.ResourceType.Folder, Sidvall.IO.FileSystemOperation.GetNames, context, cancellationToken).ConfigureAwait(false);
            return GetPaths(Sidvall.IO.Security.ResourceType.Folder, Sidvall.IO.FileSystemOperation.Read, items);
        }

        #endregion
        #region GetFileSystemEntryNamesAsync

        public async Task<IEnumerable<string>> GetFileSystemEntryNamesAsync(string path, string searchPattern, SearchOption searchOption,
            CancellationToken cancellationToken)
        {
            path = GetStoragePath(path);
            var context = new GetNamesContext()
            {
                Path = path,
                SearchOption = searchOption,
                SearchPattern = searchPattern,
            };
            var items = await DoActionAsync<IEnumerable<string>>(Sidvall.IO.Security.ResourceType.FileSystemEntry, Sidvall.IO.FileSystemOperation.GetNames, context, cancellationToken).ConfigureAwait(false);
            return GetPaths(Sidvall.IO.Security.ResourceType.FileSystemEntry, Sidvall.IO.FileSystemOperation.Read, items);
        }

        #endregion

        #region GetFileAsync

        public async Task<File> GetFileAsync(string fileName, bool loadFileDetails, CancellationToken cancellationToken)
        {
            fileName = GetStoragePath(fileName);
            var context = new GetItemContext()
            {
                Path = fileName,
                LoadDetails = loadFileDetails,
            };
            var item = await DoActionAsync<File>(Sidvall.IO.Security.ResourceType.File, Sidvall.IO.FileSystemOperation.GetItem, context, cancellationToken).ConfigureAwait(false);
            if (item != null)
                item.FullName = GetPath(item.FullName, Sidvall.IO.Security.ResourceType.File, Sidvall.IO.FileSystemOperation.Read);
            return item;
        }

        #endregion
        #region GetFilesAsync

        public async Task<IEnumerable<File>> GetFilesAsync(string path, string searchPattern, SearchOption searchOption, bool loadFileDetails,
             CancellationToken cancellationToken)
        {
            path = GetStoragePath(path);
            var context = new GetItemsContext()
            {
                Path = path,
                SearchOption = searchOption,
                SearchPattern = searchPattern,
                LoadDetails = loadFileDetails,
            };
            var items = await DoActionAsync<IEnumerable<File>>(Sidvall.IO.Security.ResourceType.File, Sidvall.IO.FileSystemOperation.GetItems, context, cancellationToken).ConfigureAwait(false);
            if (items != null)
            {
                foreach (var item in items)
                    item.FullName = GetPath(item.FullName, Sidvall.IO.Security.ResourceType.File, Sidvall.IO.FileSystemOperation.Read);
            }
            return items;
        }

        #endregion
        #region GetFolderAsync

        public async Task<Folder> GetFolderAsync(string folderName, bool loadFolderDetails, CancellationToken cancellationToken)
        {
            folderName = GetStoragePath(folderName);
            var context = new GetItemContext()
            {
                Path = folderName,
                LoadDetails = loadFolderDetails,
            };
            var item = await DoActionAsync<Folder>(Sidvall.IO.Security.ResourceType.Folder, Sidvall.IO.FileSystemOperation.GetItem, context, cancellationToken).ConfigureAwait(false);
            if (item != null)
                item.FullName = GetPath(Combine("true", item.FullName), Sidvall.IO.Security.ResourceType.Folder, Sidvall.IO.FileSystemOperation.GetItem);
            return item;
        }

        #endregion
        #region GetFoldersAsync

        public async Task<IEnumerable<Folder>> GetFoldersAsync(string path, string searchPattern, SearchOption searchOption, bool loadFolderDetails,
             CancellationToken cancellationToken)
        {
            path = GetStoragePath(path);
            var context = new GetItemsContext()
            {
                Path = path,
                SearchOption = searchOption,
                SearchPattern = searchPattern,
                LoadDetails = loadFolderDetails,
            };
            var items = await DoActionAsync<IEnumerable<Folder>>(Sidvall.IO.Security.ResourceType.Folder, Sidvall.IO.FileSystemOperation.GetItems, context, cancellationToken).ConfigureAwait(false);
            if (items != null)
            {
                foreach (var item in items)
                    item.FullName = GetPath(Combine("true", item.FullName), Sidvall.IO.Security.ResourceType.Folder, Sidvall.IO.FileSystemOperation.GetItem);
            }
            return items;
        }

        #endregion

        #region OpenFileAsync

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        public async Task<System.IO.Stream> OpenFileAsync(string path, FileMode mode, FileAccess access, CancellationToken cancellationToken)
        {
            path = GetStoragePath(path);
            var stream = new NetStream()
            {
                NetFileSystem = this,
                Path = path,
            };
            if (await FileExistsAsync(path, cancellationToken).ConfigureAwait(false))
            {
                var length = await GetLengthAsync(path, cancellationToken).ConfigureAwait(false);
                stream.SetLength(length);
            }
            else
            {
                stream.SetLength(0);
            }
            return stream;
        }

        #endregion

        #region CopyFileAsync

        public async Task<bool> CopyFileAsync(string sourceFileName, string destinationFileName, bool deleteExistingFile, CancellationToken cancellationToken)
        {
            sourceFileName = GetStoragePath(sourceFileName);
            destinationFileName = GetStoragePath(destinationFileName);
            var context = new CopyItemContext()
            {
                SourcePath = sourceFileName,
                DestinationPath = destinationFileName,
                DeleteExisting = deleteExistingFile,
            };
            return await DoActionAsync<bool>(Sidvall.IO.Security.ResourceType.File, Sidvall.IO.FileSystemOperation.Copy, context, cancellationToken).ConfigureAwait(false);
        }

        #endregion
        #region MoveFileAsync

        public async Task<bool> MoveFileAsync(string sourceFileName, string destinationFileName, bool deleteExistingFile, CancellationToken cancellationToken)
        {
            sourceFileName = GetStoragePath(sourceFileName);
            destinationFileName = GetStoragePath(destinationFileName);
            var context = new CopyItemContext()
            {
                SourcePath = sourceFileName,
                DestinationPath = destinationFileName,
                DeleteExisting = deleteExistingFile,
            };
            return await DoActionAsync<bool>(Sidvall.IO.Security.ResourceType.File, Sidvall.IO.FileSystemOperation.Move, context, cancellationToken).ConfigureAwait(false);
        }

        #endregion
        #region MoveFolderAsync

        public async Task<bool> MoveFolderAsync(string sourceFolderName, string destinationFolderName, bool deleteExistingFolder, CancellationToken cancellationToken)
        {
            sourceFolderName = GetStoragePath(sourceFolderName);
            destinationFolderName = GetStoragePath(destinationFolderName);
            var context = new CopyItemContext()
            {
                SourcePath = sourceFolderName,
                DestinationPath = destinationFolderName,
                DeleteExisting = deleteExistingFolder,
            };
            return await DoActionAsync<bool>(Sidvall.IO.Security.ResourceType.Folder, Sidvall.IO.FileSystemOperation.Move, context, cancellationToken).ConfigureAwait(false);
        }

        #endregion

        #endregion
        #region Private Members

        #region DoActionAsync

        private async Task<T> DoActionAsync<T>(Sidvall.IO.Security.ResourceType resource, Sidvall.IO.FileSystemOperation action, object value, CancellationToken cancellationToken)
        {
            var result = await GetResultAsync(resource, action, null, MediaType.ApplicationJson, HttpMethod.Post, value, null, cancellationToken).ConfigureAwait(false);
            return await result.GetContentResolvedAsync<T>().ConfigureAwait(false);
        }

        #endregion
        #region GetHttpClient

        private IHttpClient GetHttpClient(MediaType? responseType, RangeHeaderValue range)
        {
            var settings = new HttpClientSettings()
            {
                BaseAddress = this.BaseAddress,
            };
            settings.Range = range;
            settings.ResponseTypes.Clear();
            if (responseType != null)
                settings.ResponseTypes.Add(responseType.Value);
            if (this.AuthenticationStrategy != null)
                settings.AuthenticationHeader = this.AuthenticationStrategy.CurrentAuthenticationHeader;
            return Sidvall.SystemContext.Current.HttpClientFactory.GetHttpClient(settings);
        }

        #endregion
        #region GetLengthAsync

        internal async Task<long> GetLengthAsync(string path, CancellationToken cancellationToken)
        {
            HttpResponse result;

            if (this.NetFileSystemStrategy != null)
            {
                var strategyResult = await this.NetFileSystemStrategy.GetLengthAsync(path, cancellationToken).ConfigureAwait(false);
                if (strategyResult != null)
                    return strategyResult.Value;
            }
            if (this.UsePost)
            {
                var context = new ItemContext()
                {
                    Path = path,
                };
                result = await GetResultAsync(Sidvall.IO.Security.ResourceType.File, Sidvall.IO.FileSystemOperation.GetSize, null,
                    MediaType.ApplicationJson, HttpMethod.Post, context, null, cancellationToken).ConfigureAwait(false);
                return await result.GetContentResolvedAsync<long>().ConfigureAwait(false);
            }
            else
            {
                result = await GetResultAsync(Sidvall.IO.Security.ResourceType.File, Sidvall.IO.FileSystemOperation.Read, path,
                    null, HttpMethod.Head, null, null, cancellationToken).ConfigureAwait(false);
                await result.EnsureSuccessStatusCodeAsync().ConfigureAwait(false);
                return result.Content.Headers.ContentLength.Value;
            }
        }

        #endregion
        #region GetResultAsync

        internal async Task<HttpResponse> GetResultAsync(Sidvall.IO.Security.ResourceType resource, Sidvall.IO.FileSystemOperation action, string path, MediaType? responseType,
            Sidvall.Net.HttpMethod method, object value, RangeHeaderValue range, CancellationToken cancellationToken)
        {
            string content;

            if (this.NetFileSystemStrategy != null)
            {
                responseType = this.NetFileSystemStrategy.GetHttpResponseType(resource, action, responseType);
                method = this.NetFileSystemStrategy.GetHttpMethod(resource, action, method);
                content = this.NetFileSystemStrategy.GetHttpContent(resource, action, value);
            }
            else
            {
                if (value != null)
                    content = Sidvall.Serialization.SerializerManager.Current.SerializeObject(value);
                else
                    content = null;
            }
            var webAddress = NetSystemContext.Current.GetWebAddress(this.NetFileSystemStrategy, resource, action, method, this.StoreId, path);
            var client = GetHttpClient(responseType, range);
            return await GetResultAsync(client, webAddress, method, content, true, cancellationToken).ConfigureAwait(false);
        }
        private async Task<HttpResponse> GetResultAsync(IHttpClient client, string webAddress, Sidvall.Net.HttpMethod method, string content, bool retry, CancellationToken cancellationToken)
        {
            HttpResponse item;

            switch (method)
            {
                case HttpMethod.Post:
                    if (content != null)
                        item = await client.PostAsync(webAddress, content, cancellationToken).ConfigureAwait(false);
                    else
                        item = await client.GetAsync(webAddress, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
                    break;
                default:
                    if (content != null)
                        item = await client.SendAsync(webAddress, method, content, cancellationToken).ConfigureAwait(false);
                    else
                        item = await client.SendAsync(webAddress, method, cancellationToken).ConfigureAwait(false);
                    break;
            }
            if ((retry) && (item.StatusCode == System.Net.HttpStatusCode.Unauthorized) && (this.AuthenticationStrategy != null))
            {
                if (await this.AuthenticationStrategy.NewAuthenticationHeaderFromRefreshTokenAsync().ConfigureAwait(false))
                {
                    client.AuthenticationHeader = this.AuthenticationStrategy.CurrentAuthenticationHeader;
                    if (client.AuthenticationHeader != null)
                    {
                        switch (method)
                        {
                            case HttpMethod.Post:
                                if (content != null)
                                    item = await client.PostAsync(webAddress, content, cancellationToken).ConfigureAwait(false);
                                else
                                    item = await client.GetAsync(webAddress, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
                                break;
                            default:
                                if (content != null)
                                    item = await client.SendAsync(webAddress, method, content, cancellationToken).ConfigureAwait(false);
                                else
                                    item = await client.SendAsync(webAddress, method, cancellationToken).ConfigureAwait(false);
                                break;
                        }
                    }
                }
            }
            return item;
        }

        #endregion
        #region GetPaths

        private List<string> GetPaths(Sidvall.IO.Security.ResourceType resource, Sidvall.IO.FileSystemOperation action, IEnumerable<string> paths)
        {
            string fullPath;

            var list = new List<string>();
            if (paths != null)
            {
                foreach (var path in paths)
                {
                    switch (resource)
                    {
                        case Security.ResourceType.Folder:
                            fullPath = GetPath(Combine("true", path), resource, action);
                            break;
                        default:
                            fullPath = GetPath(path, resource, action);
                            break;
                    }
                    list.Add(fullPath);
                }
            }
            return list;
        }

        #endregion

        #endregion
    }
}
